import wx
import sys, os, re, time
from datetime import datetime

def log(text):
	logFile = open('error.log', 'a')
	logFile.write(repr(text) + os.linesep)
	logFile.close()
	print text

class Pyst(wx.App):
	window = None
	menu = None
	statusBar = None
	icon = None
	#taskbarIcon = None
	
	platform = sys.platform
	settings = {}
	
	modified = False
	hosts = {}
	BUTTON_ADD_NEW_HOST = 1001
	
	destination_previous = None
	
	def OnInit(self):
		success = self.readSettings()
		if not success:
			return False
		
		self.window = wx.Frame(None, -1, 'Pyst', size=wx.Size(640, 200))
		
		programName = sys.executable
		self.icon = wx.IconFromLocation(wx.IconLocation(programName, 0))
		#self.icon = wx.Icon('pyst.ico', wx.BITMAP_TYPE_ICO)
		
		self.window.SetIcon(self.icon)
		#self.taskbarIcon = wx.TaskBarIcon()
		#self.taskbarIcon.SetIcon(self.icon)
		self.addHost_icon = wx.Image('addNewHost.ico', type=wx.BITMAP_TYPE_ICO).ConvertToBitmap()
		
		self.menu = self.Menu()
		self.window.SetMenuBar(self.menu.menu)
		self.window.CreateStatusBar()
		self.statusBar = self.window.GetStatusBar()
		self.statusBar.SetFieldsCount(2)
		self.statusBar.SetStatusWidths([-1, 150])
		self.statusBar.SetFields(('Loaded hosts file: {path}'.format(path=self.getHostsPath()), ''))
		# self.window.Center()
		
		self.rightClickMenu = wx.Menu()
		self.rightClickMenu.Append(self.menu.MENU_EDIT_RENAME_HOST, 'Edit')
		self.rightClickMenu.Append(self.menu.MENU_EDIT_REMOVE_HOST, 'Delete')
		
		self.updateBackupHelp()
		
		# Prepare settings menu to reflect user preferences
		self.menu[self.menu.MENU_SETTINGS_ALWAYS_SAVE].Check(self.settings['AlwaysSaveOnClose'])
		self.menu[self.menu.MENU_SETTINGS_PROMPT_SAVE].Check(self.settings['PromptOnSave'])
		self.menu[self.menu.MENU_SETTINGS_REMEMBER_BACKUP].Check(self.settings['RememberBackupDirectory'])
		self.menu[self.menu.MENU_SETTINGS_BACKUP_DIR].Enable(self.settings['RememberBackupDirectory'])
		
		# Create primary elements
		self.panel = wx.Panel(self.window, wx.NewId())
		self.wrapper = wx.BoxSizer(wx.VERTICAL)
		self.panel.SetSizer(self.wrapper)
		
		# Visual Components
		#		Hosts list / Alias list
		self.row1 = wx.BoxSizer(wx.HORIZONTAL)
		self.destinations = wx.ListBox(self.panel, wx.NewId())
		self.destinations.SetToolTip(wx.ToolTip('Original IP/URL'))
		self.row1.Add(self.destinations, proportion=1, flag=wx.EXPAND | wx.ALL)
		self.aliases = wx.TextCtrl(self.panel, wx.NewId(), style=wx.TE_MULTILINE)
		self.aliases.SetToolTip(wx.ToolTip('Aliases'))
		self.row1.Add(self.aliases, proportion=1, flag=wx.EXPAND | wx.ALL)
		self.wrapper.Add(self.row1, proportion=1, flag=wx.EXPAND | wx.ALL)
		
		#		Add button / new host name textbox
		self.row2 = wx.BoxSizer(wx.HORIZONTAL)
		self.addHostButton = wx.BitmapButton(self.panel, self.BUTTON_ADD_NEW_HOST, bitmap=self.addHost_icon, size=(self.addHost_icon.GetWidth()+5, self.addHost_icon.GetHeight()+5))
		self.addHostButton.SetToolTip(wx.ToolTip('Add a new host'))
		self.row2.Add(self.addHostButton, proportion=0)
		self.addHostName = wx.TextCtrl(self.panel, wx.NewId(), '')
		self.addHostName.SetToolTip(wx.ToolTip('Enter the original IP/URL'))
		self.row2.Add(self.addHostName, proportion=1)
		self.wrapper.Add(self.row2, proportion=0, flag=wx.EXPAND)
		
		# Read the existing hosts file
		self.readHosts()
		
		# Listen for events
		self.aliases.Bind(wx.EVT_KEY_UP, self.AliasChange, id=self.aliases.GetId())
		self.destinations.Bind(wx.EVT_RIGHT_DOWN, self.RightClick_Destination)

		self.window.Bind(wx.EVT_MENU, self.Menu_Save, id=self.menu.MENU_FILE_SAVE)
		self.window.Bind(wx.EVT_MENU, self.Menu_SaveAs, id=self.menu.MENU_FILE_SAVEAS)
		self.window.Bind(wx.EVT_MENU, self.Menu_Backup, id=self.menu.MENU_FILE_BACKUP)
		self.window.Bind(wx.EVT_MENU, self.Menu_Revert, id=self.menu.MENU_FILE_REVERT)
		self.window.Bind(wx.EVT_MENU, self.Menu_Merge, id=self.menu.MENU_FILE_MERGE)
		self.window.Bind(wx.EVT_MENU, self.Menu_Quit, id=self.menu.MENU_FILE_QUIT)

		self.window.Bind(wx.EVT_MENU, self.Menu_AddNewHost, id=self.menu.MENU_EDIT_ADD_HOST)
		self.window.Bind(wx.EVT_MENU, self.Menu_RenameHost, id=self.menu.MENU_EDIT_RENAME_HOST)
		self.window.Bind(wx.EVT_MENU, self.Menu_RemoveHost, id=self.menu.MENU_EDIT_REMOVE_HOST)

		self.window.Bind(wx.EVT_MENU, self.Menu_AlwaysSave, id=self.menu.MENU_SETTINGS_ALWAYS_SAVE)
		self.window.Bind(wx.EVT_MENU, self.Menu_PromptOnSave, id=self.menu.MENU_SETTINGS_PROMPT_SAVE)
		self.window.Bind(wx.EVT_MENU, self.Menu_RememberBackupDirectory, id=self.menu.MENU_SETTINGS_REMEMBER_BACKUP)
		self.window.Bind(wx.EVT_MENU, self.Menu_BackupDirectory, id=self.menu.MENU_SETTINGS_BACKUP_DIR)
		self.window.Bind(wx.EVT_CLOSE, self.OnClose)
		self.window.Bind(wx.EVT_LISTBOX, self.DestinationChange, id=self.destinations.GetId())
		self.window.Bind(wx.EVT_BUTTON, self.Button_AddNewHost, id=self.BUTTON_ADD_NEW_HOST)
		
		self.window.Show()
		
		self.updateDisplay()
		
		if not self.hosts:
			dialog = wx.MessageDialog(self.window, 'Your hosts file is empty.\n\nTo add entries, enter the host name below and press the plus button.', caption='Empty hosts file', style=wx.OK | wx.ICON_INFORMATION)
			dialog.ShowModal()
			dialog.Destroy()
		
		return True
	def Menu_Save(self, event=None):
		answer = wx.ID_YES
		if self.settings['PromptOnSave']:
			dialog = wx.MessageDialog(self.window, 'Replace your existing hosts file?', caption='Save', style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
			answer = dialog.ShowModal()
			dialog.Destroy()
		if answer == wx.ID_YES:
			if self.flushHosts():
				self.statusBar.SetStatusText('Saved: {path}'.format(path=self.getHostsPath()))
				self.setModified(False)
	def Menu_SaveAs(self, event, fileName='hosts', dialogName='Save As...', showPrompt=True, chosenPath=None):
		answer = wx.ID_OK
		if showPrompt:
			dialog = wx.FileDialog(self.window, dialogName, defaultFile=fileName, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
			answer = dialog.ShowModal()
			chosenPath = dialog.GetPath()
			dialog.Destroy()
		if answer == wx.ID_OK:
			if not showPrompt:
				chosenPath += '--{postfix}'.format(postfix='-'.join(str(i) for i in datetime.now().timetuple()[:6]))
			chosenPath = os.path.normpath(chosenPath)
			self.flushHosts(path=chosenPath)
			self.statusBar.SetStatusText('Saved: {path}'.format(path=chosenPath))
			return chosenPath
	def Menu_Backup(self, event):
		usingRememberedLocation = self.settings['RememberBackupDirectory'] and self.settings['BackupDirectory']
		savedPath = self.Menu_SaveAs(event, fileName='hosts_backup', dialogName='Backup', showPrompt=not usingRememberedLocation, chosenPath='{dir}/hosts_backup'.format(dir=self.settings['BackupDirectory']))
		
		if usingRememberedLocation:
			# Location is remembered, so do it without requiring user interaction
			dialog = wx.MessageDialog(self.window, 'Backup successful.\nFile saved as "{path}"'.format(path=savedPath), caption='Backup', style=wx.OK | wx.ICON_INFORMATION)
			answer = dialog.ShowModal()
			dialog.Destroy()
		elif self.settings['RememberBackupDirectory'] and savedPath and not self.settings['BackupDirectory']:
			# Offer to remember this directory for future backups
			# savedPath will be None if the user canceled the operation
			dialog = wx.MessageDialog(self.window, 'Would you like to remember this directory for future backups?\nThis setting can be changed later from the settings menu.', caption='Backup', style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
			answer = dialog.ShowModal()
			dialog.Destroy()
			
			# Get the answer from the prompt
			remember = False
			if answer == wx.ID_YES:
				remember = True
				self.settings['BackupDirectory'] = os.path.dirname(savedPath)
				self.updateBackupHelp()
			# Make the menu reflect the change of settings
			self.settings['RememberBackupDirectory'] = remember
			self.menu[self.menu.MENU_SETTINGS_REMEMBER_BACKUP].Check(remember)
			self.menu[self.menu.MENU_SETTINGS_BACKUP_DIR].Enable(remember)
	def Menu_Revert(self, event, merge=False):
		if self.modified and not merge:
			dialog = wx.MessageDialog(self.window, 'Discard changes?', caption='Discard changes?', style=wx.YES_NO | wx.ICON_QUESTION)
			answer = dialog.ShowModal()
			dialog.Destroy()
			
			if answer == wx.ID_NO:
				return
		defaultDirectory = self.settings['BackupDirectory']
		if not defaultDirectory:
			defaultDirectory = ''
		dialog = wx.FileDialog(self.window, 'Revert...', defaultDir=defaultDirectory, defaultFile='hosts')
		answer = dialog.ShowModal()
		chosenPath = dialog.GetPath()
		dialog.Destroy()
		
		if answer == wx.ID_OK:
			self.readHosts(chosenPath, merge=merge)
	def Menu_Merge(self, event):
		self.Menu_Revert(event, merge=True)
	def Menu_Quit(self, event):
		self.window.Close()
	def Menu_AlwaysSave(self, event):
		self.settings['AlwaysSaveOnClose']  = event.Checked()
		self.saveSettings()
	def Menu_PromptOnSave(self, event):
		self.settings['PromptOnSave'] = event.Checked()
		self.saveSettings()
	def Menu_RememberBackupDirectory(self, event):
		self.settings['RememberBackupDirectory']  = event.Checked()
		self.menu[self.menu.MENU_SETTINGS_BACKUP_DIR].Enable(event.Checked())
		self.saveSettings()
	def Menu_BackupDirectory(self, event):
		dialog = wx.DirDialog(self.window, 'Backup directory...')
		answer = dialog.ShowModal()
		chosenPath = dialog.GetPath()
		dialog.Destroy()
		
		if answer == wx.ID_OK:
			self.settings['BackupDirectory'] = chosenPath
			self.saveSettings()
	def Menu_AddNewHost(self, event):
		self.Button_AddNewHost(event)
	def Menu_RenameHost(self, event):
		self.RightClick_RenameHost(event)
	def Menu_RemoveHost(self, event):
		self.RightClick_RemoveHost(event)
	def Button_AddNewHost(self, event):
		self.setModified()
		
		# Pop the textbox value
		newHostName = self.addHostName.GetValue().strip()
		self.addHostName.SetValue('')
		
		if not newHostName:
			newHostName = 'New - {timestamp}'.format(timestamp=time.time())
		
		if newHostName not in self.hosts:
			self.hosts[newHostName] = ['']
		
		self.updateDisplay()
		
		# Highlight entry
		entryIndex = self.destinations.GetItems().index(newHostName)
		self.destinations.Select(entryIndex)
		self.destinations.EnsureVisible(entryIndex)
		self.DestinationChange()
		self.aliases.SetFocus()
	def RightClick_Destination(self, event):
		rightClickMenu = self.window.PopupMenu(self.rightClickMenu)
	def RightClick_RenameHost(self, event):
		name = self.destinations.GetStringSelection()
		
		dialog = wx.TextEntryDialog(self.window, 'Change original hostname:', 'Rename')
		dialog.SetValue(name)
		answer = dialog.ShowModal()
		newName = dialog.GetValue()
		dialog.Destroy()
		
		if name != newName:
			if newName not in self.hosts:
				self.hosts[newName] = self.hosts[name]
				self.setModified()
			else:
				self.resolveDuplicateHostname(name, newName)
			
			if self.modified:
				del self.hosts[name]
				self.destination_previous = None
				self.updateDisplay()
	def RightClick_RemoveHost(self, event):
		try:
			del self.hosts[self.destinations.GetStringSelection()]
			self.setModified()
			self.updateDisplay()
		except KeyError: # User didn't have a selection
			pass
	def OnClose(self, event):
		# Program window is trying to close
		answer = None
		if self.modified and not self.settings['AlwaysSaveOnClose']:
			dialog = wx.MessageDialog(self.window, 'You have unsaved changes.  Save them now?', caption='Quit', style=wx.YES_NO | wx.CANCEL | wx.YES_DEFAULT | wx.ICON_QUESTION)
			answer = dialog.ShowModal()
			dialog.Destroy()
		
		if self.settings['AlwaysSaveOnClose'] or answer == wx.ID_YES:
			self.Menu_Save()
		elif answer == wx.ID_CANCEL:
			print "Cancel"
			event.Veto()
			return
		
		#self.taskbarIcon.Destroy()
		self.window.Destroy()	
	def resolveDuplicateHostname(name, newName):
		dialog = wx.MessageDialog(self.window, 'A host "{hostname}" already exists.  Merge the two entries?'.format(hostname=name), caption='Duplicate host', style=wx.OK | wx.CANCEL | wx.ICON_QUESTION)
		answer = dialog.ShowModal()
		if answer == wx.ID_OK:
			self.hosts[newName].extend(self.hosts[name])
			self.setModified()
		return asnwer == wx.ID_OK
	def readHosts(self, path=None, merge=False):
		if not path:
			path = self.getHostsPath()
		hostsFile = open(path, 'rU')
		
		if not merge:
			self.hosts = {}
		for rawLine in hostsFile:
			line = rawLine.strip()
			if '#' in line:
				line = line[:line.index('#')]
			if line:
				destination, aliases = re.split(r'\s', line, 1)
				if destination not in self.hosts:
					self.hosts[destination] = []
				try:
					hostsLine = self.unique(unicode(i) for i in re.split(ur'\s*,\s*', unicode(aliases)))
				except UnicodeDecodeError:
					log('The following alias has innapropriate unicode characters: %s' % aliases)
					continue
				self.hosts[destination].extend(hostsLine)
		hostsFile.close()
		self.setModified(merge)
		self.updateDisplay()
		return True
	def flushHosts(self, path=None):
		if not path:
			path = self.getHostsPath()
		self.DestinationChange()
		try:
			hostsFile = open(path, 'w')
			for destination,aliases in self.hosts.items():
				hostsFile.write('{destination}\t{aliases}\n'.format(destination=destination, aliases=', '.join(aliases)))
			self.modified = False
			hostsFile.close()
		except IOError as (errorNumber, message):
			dialog = wx.MessageDialog(self.window, 'An error occured while trying to write the file:\n{path}\n\nError number {errNum}: {message}\n\nWindows Vista/7 users:\nIf enabled, UAC prevents alterations to this file or location.\n\nConsider making a backup file in a different location.'.format(path=path, errNum=errorNumber, message=message), caption='Error', style=wx.OK | wx.ICON_ERROR)
			dialog.ShowModal()
			dialog.Destroy()
		except:
			dialog = wx.MessageDialog(self.window, 'Unexpected error:\n{info}'.format(info=sys.exc_info()[0]), caption='Error', style=wx.OK | wx.ICON_ERROR)
			dialog.ShowModal()
			dialog.Destroy()
		# Success status
		return not self.modified
	def getHostsPath(self):
		return 'C:/Windows/system32/drivers/etc/hosts'
	def readSettings(self):
		# Ensure defaults if keys are missing
		defaults = {
			'PromptOnSave'				: True,
			'AlwaysSaveOnClose'		: False,
			'RememberBackupDirectory'	: True,
			'BackupDirectory'			: '',
		}
		if not os.path.exists('settings.ini'):
			self.settings = defaults
		else:
			settingsFile = open('settings.ini')
			self.settings = {}
			for line in settingsFile:
				try:
					property, value = re.split(r'\s*=\s*', line.strip(), 1)
					try:
						if value == '':
							value = 'None'
						self.settings[property] = eval(value)
					except:
						log("Property '{property}' cannot be processed.  Its value is: '{value}'".format(property=property, value=value))
				except ValueError:
					log("The following line does not contain a valid pair: {line}".format(line=line))
				for property in defaults:
					if property not in self.settings:
						self.settings[property] = defaults[property]
		self.saveSettings()
		return True
	def saveSettings(self, defaults=False):
		settingsFile = open('settings.ini', 'w')
		for property, value in self.settings.items():
			settingsFile.write('{property} = {value}\n'.format(property=property, value=value))
		settingsFile.close()
	def updateBackupHelp(self):
		backupMenuItem = self.menu[self.menu.MENU_SETTINGS_REMEMBER_BACKUP]
		backupMenuItem.SetHelp('Future backups will automatically be saved in the chosen location.  Currently: {path}'.format(path=self.settings['BackupDirectory']))
		backupMenuItem = self.menu[self.menu.MENU_SETTINGS_BACKUP_DIR]
		backupMenuItem.SetHelp('Choose where future backups are saved.  Currently: {path}'.format(path=self.settings['BackupDirectory']))

	def DestinationChange(self, event=None):
		# Commits changes made to the alias list
		self.aliases.Enable(True)
		if self.destination_previous:
			self.AliasChange()
		self.destination_previous = self.destinations.GetStringSelection()
		
		# Refresh Aliases for the new destination
		self.updateAliases()
	def AliasChange(self, event=None):
		alteredAliases = self.unique(self.getActualAliasList())
		self.setModified(self.modified or self.hosts[self.destination_previous] != alteredAliases) # This isn't working as expected yet... should undo 'modified' status if things match
		if self.modified:
			self.hosts[self.destination_previous] = alteredAliases
		if event:
			event.Skip() # Propogate keystroke to UI
		return self.modified
	def getActualAliasList(self):
		return [i for i in [unicode(re.sub(r'\s*', '', i)) for i in re.split('[\n,;]', self.aliases.GetValue())] if i]
	def unique(self, iterable):
		return tuple(sorted(set(iterable), key=unicode.lower))
	def setModified(self, modified=True):
		self.modified = modified
		self.statusBar.SetStatusText('Modified'*modified, 1)
	def updateDisplay(self):
		self.destination_previous = None
		self.destinations.Set(self.unique(unicode(host) for host in self.hosts.keys()))
		self.aliases.Enable(False)

		self.updateAliases() # Will always disable the alias box
	def updateAliases(self):
		destination = self.destinations.GetStringSelection()
		if destination:
			self.aliases.SetValue('\n'.join(self.unique(unicode(i).strip() for i in self.hosts[destination])))
		else:
			self.aliases.SetValue('')
	class Menu:
		menu = None
		MENU_FILE =										100
		MENU_FILE_SAVE =								101
		MENU_FILE_SAVEAS =							102
		MENU_FILE_BACKUP =							103
		MENU_FILE_REVERT =							104
		MENU_FILE_MERGE =								105
		MENU_FILE_QUIT =								199
		MENU_EDIT =									200
		MENU_EDIT_ADD_HOST =						201
		MENU_EDIT_RENAME_HOST =					202
		MENU_EDIT_REMOVE_HOST =				203
		MENU_SETTINGS =							300
		MENU_SETTINGS_ALWAYS_SAVE =			301
		MENU_SETTINGS_PROMPT_SAVE =			302
		MENU_SETTINGS_REMEMBER_BACKUP =	303
		MENU_SETTINGS_BACKUP_DIR =			304
		def __init__(self):
			self.menu = wx.MenuBar()

			self.menu_file = wx.Menu()
			self.menu_file.Append(self.MENU_FILE_SAVE, '&Save\tCtrl+S', 'Commit your changes')
			self.menu_file.Append(self.MENU_FILE_SAVEAS, 'Save &As...\tCtrl+Shift+S', 'Save current changes to a new file')
			self.menu_file.AppendSeparator()
			self.menu_file.Append(self.MENU_FILE_BACKUP, '&Backup...\tCtrl+B', 'Save a backup of the existing file (without any current changes)')
			self.menu_file.Append(self.MENU_FILE_REVERT, '&Revert...\tCtrl+R', 'Revert to previous backup')
			self.menu_file.Append(self.MENU_FILE_MERGE, '&Merge...\tCtrl+M', 'Merge an existing hosts file with your existing one')
			self.menu_file.AppendSeparator()
			self.menu_file.Append(self.MENU_FILE_QUIT, '&Quit\tCtrl+W', 'Exit')

			self.menu_edit = wx.Menu()
			self.menu_edit.Append(self.MENU_EDIT_ADD_HOST, '&Add new host\tCtrl+N', 'Add a line to this hosts file')
			self.menu_edit.Append(self.MENU_EDIT_RENAME_HOST, '&Edit selected host name...\tCtrl+E', 'Change the selected host')
			self.menu_edit.Append(self.MENU_EDIT_REMOVE_HOST, '&Delete selected host\tCtrl+Del', 'Remove the selected host')
			
			self.menu_settings = wx.Menu()
			self.menu_settings.Append(self.MENU_SETTINGS_ALWAYS_SAVE, '&Always save on exit', 'Automatically save changes when exiting.  (You will not be prompted.)', kind=wx.ITEM_CHECK)
			self.menu_settings.Append(self.MENU_SETTINGS_PROMPT_SAVE, '&Prompt hosts overwrite', 'Require confirmation when overwriting your main hosts file', kind=wx.ITEM_CHECK)
			self.menu_settings.AppendSeparator()
			self.menu_settings.Append(self.MENU_SETTINGS_REMEMBER_BACKUP, '&Remember backup directory', 'Future backups will automatically be saved in the chosen location', kind=wx.ITEM_CHECK)
			self.menu_settings.Append(self.MENU_SETTINGS_BACKUP_DIR, '&Backup directory...', 'Choose where future backups are saved')
			
			# With icon
			#MenuItem quit = wxMenuItem(parentMenu, id, text, statusMessage, wx.ITEM_NORMAL | wx.ITEM_CHECK | wx.ITEM_RADIO)
			
			self.menu.Append(self.menu_file, '&File')
			self.menu.Append(self.menu_edit, '&Edit')
			self.menu.Append(self.menu_settings, '&Settings')
		def __getitem__(self, key):
			return self.menu.FindItemById(key)
app = Pyst(0)
app.MainLoop()
